﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DDD.Core {
    /// <summary>
    /// スケレタルアニメーションのためのスキンメッシュ･コンポーネント
    /// </summary>
    /// <remarks>
    /// このクラスは<see cref=" Transformer"/>の一種でボーンによるメッシュの変形を許すスケレタルアニメーションのためのクラスです。
    /// DDDのボーンアニメーションはJason GregoryのGame Engine Arhitectureを元に実装されています。
    /// </remarks>
    public class SkinnedMesh : Transformer {

        #region Field
        Node[] bones;
        Matrix4x4[] invBindPoses;
        #endregion

        #region Constructor
        /// <summary>
        /// サブメッシュ１つのコンストラクター
        /// </summary>
        public SkinnedMesh ()
            : this (1) {
        }

        /// <summary>
        /// 複数のサブメッシュのコンストラクター
        /// </summary>
        /// <param name="numSubmesh">サブメッシュの数</param>
        public SkinnedMesh (int numSubmesh)
            : base (numSubmesh) {
            this.bones = new Node[0];
            this.invBindPoses = new Matrix4x4[0];
        }
        #endregion

        #region Property
        /// <summary>
        /// ボーン数
        /// </summary>
        /// <remarks>
        /// 現在設定されているボーンの数を返します。
        /// </remarks>
        public int BoneCount {
            get { return bones.Length; }
        }

        /// <summary>
        /// 設定済みの全てのボーンを列挙する列挙子
        /// </summary>
        /// <remarks>
        /// 現在設定されているボーンノードを全て列挙します。
        /// ボーンノードは<see cref="SetBones"/>メソッドで設定します。
        /// </remarks>
        public IEnumerable<Node> Bones {
            get { return bones; }
        }

        /// <summary>
        /// このオブジェクトが参照する全DDDオブジェクトを列挙する列挙子
        /// </summary>
        /// <remarks>
        /// 参照にnullは含まれません。同一のオブジェクトが複数出現する可能性があります。
        /// </remarks>
        public override IEnumerable<Object> References {
            get {
                var objs = base.References.ToList ();
                objs.AddRange (bones);
                objs.RemoveAll (x=>x==null);
                return objs;
            }
        }
        #endregion

        #region Method

        /// <summary>
        /// ボーンをセットします
        /// </summary>
        /// <remarks>
        /// ボーンはこのメソッドを使って全て1回でセットします。
        /// 以前設定されていたボーンおよびボーン行列は削除されます。
        /// ボーンは「ボーン番号」の順に配列に格納して渡します。ボーン番号は後でボーン行列にアクセスするときに必要です。
        /// このメソッドを呼び出したときの姿勢がバインドポーズとなります。
        /// ボーン座標変換行列(invBindPose)は自動的にこのタイミングで計算され保管されます。
        /// ボーンはすべてこのコンポーネントがアタッチされたノードの子ノードである必要があります。
        /// </remarks>
        /// <param name="bones">ボーンノードの配列</param>
        public void SetBones (Node[] bones) {
            if (bones == null) {
                throw new ArgumentNullException ("Bones is null");
            }
            if (bones.Any (x => x == null)) {
                throw new ArgumentNullException ("Bones has null node");
            }
            if (Node == null) {
                throw new InvalidOperationException ("This component is not attached");
            }
            if (bones.Any (x => !x.Upwards.Contains (Node))) {
                throw new InvalidOperationException ("Some bones are not under this Node");
            }

            this.bones = (Node[])bones.Clone();
            this.invBindPoses = new Matrix4x4[bones.Length];
            for (var i = 0; i < bones.Length; i++) {
                this.invBindPoses[i] =  Node.GetTransformTo (bones[i]);
            }
        }

        /// <summary>
        /// 全てのボーンを削除します
        /// </summary>
        public void RemoveBones () {
            this.bones = new Node[0];
            this.invBindPoses = new Matrix4x4[0];
        }

        /// <summary>
        /// ボーン(ノード)を取得します
        /// </summary>
        /// <param name="index">ボーン番号</param>
        /// <returns>ボーンノード</returns>
        public Node GetBone (int index) {
            if (BoneCount == 0) {
                throw new InvalidOperationException ("Bone is not set");
            }
            if (index < 0 || index >= BoneCount) {
                throw new IndexOutOfRangeException ("Index is out of range");
            }
            return bones[index];
        }

        /// <summary>
        /// ボーン インデックスの取得
        /// </summary>
        /// <remarks>
        /// 指定のノード <paramref name="bone"/> がボーンとして登録されていない場合、例外が発生します。
        /// </remarks>
        /// <param name="bone">ボーン ノード</param>
        /// <returns>インデックス</returns>
        public int GetBoneIndex (Node bone) {
            throw new NotImplementedException ("Sorry");
        }

        /// <summary>
        /// バインド姿勢の時のボーン座標変換行列(invBindPose)を取得します
        /// </summary>
        /// <remarks>
        /// この行列はバインド姿勢のときのモデリング座標からボーン座標に変換します。
        /// </remarks>
        /// <param name="index">ボーン番号</param>
        /// <returns>逆バインドポーズ行列</returns>
        public Matrix4x4 GetInvBindPoseMatrix (int index) {
            if (BoneCount == 0) {
                throw new InvalidOperationException ("Bone is not set");
            }
            if (index < 0 || index >= BoneCount) {
                throw new IndexOutOfRangeException ("Index is out of range");
            }
            return invBindPoses[index];
        }

        /// <summary>
        /// 現在の姿勢のポーズ行列を取得します
        /// </summary>
        /// <remarks>
        /// カレントのポーズ行列とは現在の姿勢の時のボーン座標からモデリング座標への変換行列です。
        /// </remarks>
        /// <param name="index">ボーン番号</param>
        /// <returns>カレントのポーズ行列</returns>
        public Matrix4x4 GetCurrentPoseMatrix (int index) {
            if (index < 0 || index >= BoneCount) {
                throw new IndexOutOfRangeException ("Index is out of range");
            }

            return bones[index].GetTransformTo (Node);
        }

        /// <summary>
        /// スキニング行列を取得します
        /// </summary>
        /// <remarks>
        /// スキニング行列はバインド姿勢の時の頂点をカレントの姿勢の位置に移動します。
        /// <code>InvBindPoseMatrix</code>に<code>CurrentPoseMatrix</code>をかけた物に等しいです。
        /// </remarks>
        /// <param name="index">ボーン番号</param>
        /// <returns>スキニング行列</returns>
        public Matrix4x4 GetSkinningMatrix (int index) {
            if (index < 0 || index >= BoneCount) {
                throw new IndexOutOfRangeException ("Index is out of range");
            }
            return GetCurrentPoseMatrix(index) * GetInvBindPoseMatrix(index);
        }

        /// <summary>
        /// 法線用のスキニング行列を取得します
        /// </summary>
        /// <remarks>
        /// 定義から法線用のスキニング行列は通常のスキニング行列の3x3要素の逆行列の転置に等しいです。
        /// </remarks>
        /// <param name="index">ボーン番号</param>
        /// <returns>法線用のスキニング行列</returns>
        public Matrix3x3 GetSkinningMatrix3x3 (int index) {
            if (index < 0 || index >= BoneCount) {
                throw new IndexOutOfRangeException ("Index is out of range");
            }
            return GetSkinningMatrix (index).Matrix3x3.Inverse ().Transpose ();
        }

        /// <summary>
        /// 現在のオブジェクトの状態を表す文字列を返します。
        /// </summary>
        /// <returns>現在のオブジェクトを説明する文字列</returns>
        public override string ToString () {
            return string.Format ("SkinnedMesh: BoneCount={0}", BoneCount);
        }
        #endregion


    }
}
